using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Security.Cryptography;
using System.Runtime.Serialization;
using System.Security;
using System.Diagnostics;
using System.Security.Permissions;

namespace PasswordProvider.ObjectModel
{
    /// <summary>
    /// Encryption utility. Exposes methods to encrypt and decrypt passwords and other values. 
    /// Based on the Rijndael symmetric encryption algorithm (see RijndaelManaged class in System.Security.Cryptology)
    /// Basically, from the user entered master password and a randomly generated SALT, a 32 byte array value master key
    /// is generated. This key is then used by the RijndaelManaged algorithm to encrypt values. The same key is required
    /// to decrypt values. 
    /// </summary>
    [DataContract]
    internal sealed class Cipher
    {
        #region Events
        
        public event EventHandler DecryptedValue;
        
        #endregion

        #region Fields, Construction

        private const int _sizeOfKeyInBytes = 32;   // 32 bytes == 256 bits, largest key size allowed by Rijndael
        private const int _sizeOfSaltInBytes = 32;
        
        private byte[] _masterKey;          // This value is obtained from the user entered password using the Rijneael Cypher.
        
        #endregion // Fields, Construction

        #region System.Object overides

        /// <summary>
        /// Two RijndaelCypher are equal if both masterKeySalt and masterKeyHash are equal.
        /// </summary>
        public override bool Equals(Object other)
        {
            Cipher otherCipher = other as Cipher;
            if (otherCipher != null)
            {
                if (otherCipher.MasterKeyHash.SequenceEqual(this.MasterKeyHash)
                    && otherCipher.MasterKeySalt.SequenceEqual(this.MasterKeySalt))
                {
                    return true;
                }
            }

            return false;
        }
        public override int GetHashCode()
        {
            return MasterKeyHash.GetHashCode() + MasterKeySalt.GetHashCode();
        }
        
        #endregion

        #region Public Properties

        /// <summary>
        /// A hash of the master key used for password validation.
        /// </summary>
        [DataMember]
        public byte[] MasterKeyHash
        {
            get;
            private set;
        }

        /// <summary>
        /// The SALT used to generate the Master Key from the user entered password.
        /// </summary>
        [DataMember]
        public byte[] MasterKeySalt
        {
            get;
            private set;
        }

        /// <summary>
        /// Number of iterations to use in the Rfc2898DeriveBytes alg for Master Key generation. The higher the value
        /// the more secure the resulting encrypted values.
        /// </summary>
        [DataMember]
        public int MasterKeyGenerationIterations
        {
            get;
            private set;
        }

        public bool IsInitialized
        {
            get { return (MasterKeySalt != null && MasterKeyHash != null); }
        }

        public bool IsAuthenticated
        {
            get { return _masterKey != null; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Initialize a RijndaelCypher to decrypt previously encrypted values. Authenticate method must be 
        /// successfully called before the cypher can be used.
        /// </summary>
        /// <param name="MasterHash">Must be equal to value of MasterHash property from cypher previously used to encrypt values.</param>
        /// <param name="MasterSalt">Must be equal to value of MasterSalt property from cypher previously used to encrypt values.</param>
        /// <param name="MasterIterations">Must be equal to value of MasterIterations property from cypher previously used to encrypt values.</param>
        public void Initialize(byte[] masterHash, byte[] masterSalt, int masterIterations)
        {
            MasterKeyHash = masterHash;
            MasterKeySalt = masterSalt;
            MasterKeyGenerationIterations = masterIterations;
            if (_masterKey != null)
                InvalidateAuthentication();
        }

        /// <summary>
        /// Initialize with password. Use for new cypher, one that is not needed to decrypt any existing 
        /// encrypted values.
        /// </summary>
        /// <param name="password">The password used to initialize the cypher. This is used to generate the key to the 
        /// cypher and is the value that must be passed to Authenticate method to use the cypher for future encryptions.</param>
        public void SetPassword(ProtectedString password, int iterations)
        {
            // set SALT and iteration values.
            MasterKeySalt = GenerateSalt();
            MasterKeyGenerationIterations = iterations;

            Rfc2898DeriveBytes derivedBytes = new Rfc2898DeriveBytes(password.GetUtf8Bytes(), MasterKeySalt, MasterKeyGenerationIterations);
            // Unprotect the master key memory to change it.
            if(_masterKey != null)
                ProtectedMemory.Unprotect(_masterKey, MemoryProtectionScope.SameProcess);
            // Generate the master key.
            _masterKey = derivedBytes.GetBytes(_sizeOfKeyInBytes);
            // Record the key's hash.
            MasterKeyHash = ComputeHash(_masterKey);
            // Protect the memory.
            ProtectedMemory.Protect(_masterKey, MemoryProtectionScope.SameProcess);                        
        }
        
        /// <summary>
        /// Initializes the encryption engine with the password required to generate the master key,
        /// which is required to unencrypt existing values or encrypt new ones. A hash is generated from this key 
        /// (using the SALT) and checked for equality against the hash that is set during intialization. If the two hashes are
        /// equal, the password is valid. 
        /// </summary>
        public bool Authenticate(ProtectedString masterPassword)
        {
            if (!IsInitialized)
                throw new InvalidOperationException("Cypher not initialized");

            Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(masterPassword.GetUtf8Bytes(), MasterKeySalt, MasterKeyGenerationIterations);

            byte[] key = deriveBytes.GetBytes(_sizeOfKeyInBytes);
            byte[] hash = ComputeHash(key);
            bool authenticated = hash.SequenceEqual(MasterKeyHash);
            if (authenticated)
            {
                if(_masterKey != null)
                    ProtectedMemory.Unprotect(_masterKey, MemoryProtectionScope.SameProcess);
                _masterKey = key;
                ProtectedMemory.Protect(_masterKey, MemoryProtectionScope.SameProcess);
            }
            
            return authenticated;
        }
        
        /// <summary>
        /// Clean secret by setting the masterkey bytes to 0 and then set to null.
        /// After calling this method, Authenticate must be called to use cypher again.
        /// </summary>
        public void InvalidateAuthentication()
        {
            if (_masterKey != null)
            {
                ProtectedMemory.Unprotect(_masterKey, MemoryProtectionScope.SameProcess);
                _masterKey.Initialize(); // wipe memory by setting values to 0 for security 
                _masterKey = null;
            }                        
        }
       
        public KeyValuePair<byte[], byte[]> EncryptValue(ProtectedString unsecureProtectedString)
        {
            Debug.Assert(unsecureProtectedString != null);

            return EncryptValue(unsecureProtectedString.GetUtf8Bytes());
        }
        /// <summary>
        /// Encrypts a byte array using an instance of RinjdaelManaged cypher that is initialized with the 
        /// key generated from the user entered password.
        /// </summary>
        /// <param name="input">Byte array to encrypt</param>
        /// <returns>An instance of EncryptedValue with encrypted value and salt</returns>
        public KeyValuePair<byte[], byte[]> EncryptValue(byte[] unsecureBytes)
        {
            if (!IsAuthenticated)
            {
                throw new AuthenticationException();
            }

            Debug.Assert(unsecureBytes != null);
            Debug.Assert(unsecureBytes.Length > 0);

            byte[] salt = GenerateSalt();

            byte[] value;
            using (RijndaelManaged rinj = CreateRijndaelManagedForEncryptedValue(salt))
            {
                value = rinj.CreateEncryptor().TransformFinalBlock(unsecureBytes, 0, unsecureBytes.Length);
            }

            KeyValuePair<byte[], byte[]> returnValue = new KeyValuePair<byte[], byte[]>(salt, value);
            return returnValue;
        }

        /// <summary>
        /// Decrypts the Value contained in the instance of EncryptedValue using an instance of the RijndaelManaged
        /// that is initialized with a key that has been generated from the user entered password.
        /// </summary>
        /// <param name="encryptedValue">Instance of EncryptedValue containing the value to decrypt and the salt</param>
        /// <returns>Decrypted value in a secure string</returns>
        public ProtectedString DecryptValue(byte[] value, byte[] salt)
        {
            if (!IsAuthenticated)
            {
                throw new AuthenticationException();
            }
            Debug.Assert(value != null && salt != null && value.Length > 0);

            byte[] decryptedValue = null;
            using (RijndaelManaged rinj = CreateRijndaelManagedForEncryptedValue(salt))
            {
                decryptedValue = rinj.CreateDecryptor().TransformFinalBlock(value, 0, value.Length);
            }

            if (DecryptedValue != null)
                DecryptedValue(this, new EventArgs());

            return new ProtectedString(decryptedValue);            
        }

        public RijndaelManaged CreateRijndaelManagedForFile()
        {
            RijndaelManaged cypher = new RijndaelManaged();

            ProtectedMemory.Unprotect(_masterKey, MemoryProtectionScope.SameProcess);
            cypher.Key = new SHA256Managed().ComputeHash(_masterKey);
            ProtectedMemory.Protect(_masterKey, MemoryProtectionScope.SameProcess);
            cypher.Padding = PaddingMode.ISO10126;
            cypher.Mode = CipherMode.CBC;
            cypher.GenerateIV();

            return cypher;
        }

        #endregion

        #region Private Methods

        // Create a Rijndael symmetric algorithm cypher. The salt is 
        // used to generate the key for the cypher. 
        private RijndaelManaged CreateRijndaelManagedForEncryptedValue(byte[] salt)
        {
            if (salt == null)
                throw new ArgumentNullException("salt");

            ProtectedMemory.Unprotect(_masterKey, MemoryProtectionScope.SameProcess);
            Rfc2898DeriveBytes alg = new Rfc2898DeriveBytes(_masterKey, salt, 1000);
            ProtectedMemory.Protect(_masterKey, MemoryProtectionScope.SameProcess);

            byte[] key = alg.GetBytes(_sizeOfKeyInBytes);

            RijndaelManaged cypher = new RijndaelManaged();
            cypher.Mode = CipherMode.CBC;
            cypher.Padding = PaddingMode.ISO10126;
            cypher.Key = key;
            cypher.IV = alg.GetBytes(16);

            return cypher;
        }

        // return an array of random bytes (a "salt") with givin length.
        private static byte[] GenerateSalt()
        {
            // Create a random number generator (using the one provided by the cryptography namespace
            // and use it to fill a byte array with random values.
            byte[] randBytes = new byte[_sizeOfSaltInBytes];
            RNGCryptoServiceProvider rand = new RNGCryptoServiceProvider();
            rand.GetBytes(randBytes);

            return randBytes;
        }

        // Computes a hash of the byte array using SHA512Managed alg.
        private static byte[] ComputeHash(byte[] key)
        {
            SHA512Managed alg = new SHA512Managed();
            return alg.ComputeHash(key);
        }
        #endregion

    }

}
